/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.propagation;

import ore.SpacecraftState;
import ore.errors.OrekitException;
import ore.errors.PropagationException;
import ore.propagation.events.AbstractDetector;
import ore.propagation.events.EventDetector;
import ore.propagation.events.CombinedEventsDetectorsManager;
import ore.propagation.sampling.BasicStepInterpolator;
import ore.propagation.sampling.OrekitFixedStepHandler;
import ore.propagation.sampling.OrekitStepHandler;
import ore.propagation.sampling.OrekitStepNormalizer;
import ore.time.AbsoluteDate;

import org.apache.commons.math.ConvergenceException;

import java.util.Collection;

/** 
 * Common handling of {@link Propagator} methods for analytical-like
 * propagators.
 * 
 * This abstract class allows to provide easily the full set of {@link
 * Propagator} methods, including all propagation modes support and
 * discrete events support for any simple propagation method. Only two
 * methods must be implemented by derived classes: {@link
 * #basicPropagate(AbsoluteDate)} and {@link
 * #resetInitialState(SpacecraftState)}. The first method should
 * perform straightforward propagation starting from some internally
 * stored initial state up to the specified target date. The second
 * method should reset the initial state when called.
 * 
 * @author Luc Maisonobe
 */
public abstract class AbstractPropagator
    implements Propagator
{
    /** 
     * Propagation mode.
     */
    private int mode;
    /** 
     * Fixed step size.
     */
    private double fixedStepSize;
    /** 
     * Step handler.
     */
    private OrekitStepHandler stepHandler;
    /** 
     * Manager for events detectors.
     */
    private final CombinedEventsDetectorsManager eventsDetectorsManager;
    /** 
     * Internal steps interpolator.
     */
    private final BasicStepInterpolator interpolator;


    protected AbstractPropagator() {
        super();
        this.eventsDetectorsManager = new CombinedEventsDetectorsManager();
        this.interpolator = new BasicStepInterpolator(new UnboundedPropagatorView());
        this.setSlaveMode();
    }


    /**
     * {@inheritDoc}
     */
    public int getMode() {
        return mode;
    }
    /**
     * {@inheritDoc}
     */
    public void setSlaveMode() {
        this.mode          = SLAVE_MODE;
        this.stepHandler   = null;
        this.fixedStepSize = Double.NaN;
    }
    /**
     * {@inheritDoc}
     */
    public void setMasterMode(double h, OrekitFixedStepHandler handler) {
        this.mode          = MASTER_MODE;
        this.stepHandler   = new OrekitStepNormalizer(h, handler);
        this.fixedStepSize = h;
    }
    /**
     * {@inheritDoc}
     */
    public void setMasterMode(OrekitStepHandler handler) {
        this.mode          = MASTER_MODE;
        this.stepHandler   = handler;
        this.fixedStepSize = Double.NaN;
    }
    /**
     * {@inheritDoc}
     */
    public void setEphemerisMode() {
        this.mode          = EPHEMERIS_GENERATION_MODE;
        this.stepHandler   = null;
        this.fixedStepSize = Double.NaN;
    }
    /**
     * {@inheritDoc}
     */
    public BoundedPropagator getGeneratedEphemeris() {
        return new UnboundedPropagatorView();
    }
    /**
     * {@inheritDoc}
     */
    public void addEventDetector(EventDetector detector) {
        this.eventsDetectorsManager.addEventDetector(detector);
    }
    /**
     * {@inheritDoc}
     */
    public Collection<EventDetector> getEventsDetectors() {
        return this.eventsDetectorsManager.getEventsDetectors();
    }
    /**
     * {@inheritDoc}
     */
    public void clearEventsDetectors() {
        this.eventsDetectorsManager.clearEventsDetectors();
    }
    /**
     * {@inheritDoc}
     */
    public SpacecraftState propagate(AbsoluteDate target)
        throws PropagationException
    {
        try {
            /*
             * Initial state
             */
            this.interpolator.storeDate(this.getInitialDate());
            SpacecraftState state = this.interpolator.getInterpolatedState();
            /*
             * Evaluate step size
             */
            double stepSize;
            if (mode == MASTER_MODE) {
                if (Double.isNaN(this.fixedStepSize)) {
                    stepSize = (state.getKeplerianPeriod() / 100);
                }
                else {
                    stepSize = fixedStepSize;
                }
            } 
            else {
                stepSize = target.durationFrom(this.interpolator.getCurrentDate());
            }
            final CombinedEventsDetectorsManager combinedMgr =
                this.addEndDateChecker(this.getInitialDate(), target, this.eventsDetectorsManager);
            /*
             * Iterate over the propagation range
             */
            AbsoluteDate stepEnd =
                new AbsoluteDate(this.interpolator.getCurrentDate(), stepSize);

            for (boolean lastStep = false; !lastStep;) {

                this.interpolator.shift();
                boolean needUpdate = false;
                /*
                 * Attempt to perform one step, with the current step
                 * size (this may loop if some discrete event is
                 * triggered)
                 */
                for (boolean loop = true; loop;) {
                    /*
                     * Go ahead one step size
                     */
                    this.interpolator.storeDate(stepEnd);
                    /*
                     * Check discrete events
                     */
                    if (combinedMgr.evaluateStep(this.interpolator)) {
                        needUpdate = true;
                        stepEnd = combinedMgr.getEventTime();
                    } 
                    else {
                        loop = false;
                    }
                }
                /*
                 * Handle the accepted step
                 */
                state = this.interpolator.getInterpolatedState();
                combinedMgr.stepAccepted(state);
                if (combinedMgr.stop()) {
                    lastStep = true;
                } 
                else {
                    lastStep = stepEnd.compareTo(target) >= 0;
                }
                if (stepHandler != null) {
                    stepHandler.handleStep(interpolator, lastStep);
                }
                /*
                 * Let the events detectors reset the state if needed
                 */
                final SpacecraftState newState = combinedMgr.reset(state);
                if (newState != state) {
                    this.resetInitialState(newState);
                    state = newState;
                }

                if (needUpdate) {
                    /*
                     * An event detector has reduced the step
                     * we need to adapt step size for next iteration
                     */
                    stepEnd = new AbsoluteDate(interpolator.getPreviousDate(), stepSize);
                } 
                else {
                    stepEnd = new AbsoluteDate(interpolator.getCurrentDate(), stepSize);
                }
            }
            /*
             * Return the last computed state
             */
            return state;
        }
        catch (OrekitException oe) {
            PropagationException p = oe.getCausePropagationException();
            if (null != p)
                throw p;
            else
                throw new PropagationException(oe.getMessage(), oe);
        }
        catch (ConvergenceException ce) {
            throw new PropagationException(ce.getMessage(), ce);
        }
    }
    /** 
     * @return initial propagation date
     */
    protected abstract AbsoluteDate getInitialDate();
    /** 
     * As the {@link #propagate} method, except that it does
     * <strong>not</strong> call any handler during propagation, nor
     * any discrete events. It will always stop exactly at the
     * specified date.
     * 
     * @param date Target date for propagation
     * 
     * @return State at specified date
     * 
     * @exception PropagationException Propagation cannot reach specified date
     */
    protected abstract SpacecraftState basicPropagate(AbsoluteDate date)
        throws PropagationException;
    /** 
     * Reset the basic propagator initial state.
     * 
     * @param state New initial state 
     * 
     * @exception PropagationException Initial state cannot be reset
     */
    protected abstract void resetInitialState(SpacecraftState state)
        throws PropagationException;
    /** 
     * Add an event handler for end date checking.
     * 
     * This method can be used to simplify handling of integration end
     * date.  It leverages the nominal stop condition with the
     * exceptional stop conditions.
     * 
     * @param startDate Propagation start date
     * @param endDate Desired end date
     * @param manager Manager containing the user-defined handlers
     * 
     * @return A new manager containing all the user-defined handlers plus a
     * dedicated manager triggering a stop event at entDate
     */
    protected CombinedEventsDetectorsManager addEndDateChecker(AbsoluteDate startDate, AbsoluteDate endDate,
                                                               CombinedEventsDetectorsManager manager)
    {
        CombinedEventsDetectorsManager newManager = new CombinedEventsDetectorsManager(manager);
        {
            final double dt = endDate.durationFrom(startDate);
            newManager.addEventDetector(new EndDateDetector(endDate, Double.POSITIVE_INFINITY, Math.ulp(dt)));
            return newManager;
        }
    }
    /**
     * A kind of {@link BoundedPropagator} for an unbounded view of
     * the instance.  Propagate calls basic propagate, which always
     * halts at the propagation target date.
     */
    private class UnboundedPropagatorView
        extends Object
        implements BoundedPropagator
    {
        public UnboundedPropagatorView(){
            super();
        }
        /** {@inheritDoc}
         */
        public AbsoluteDate getMinDate() {
            return AbsoluteDate.PAST_INFINITY;
        }
        /** {@inheritDoc}
         */
        public AbsoluteDate getMaxDate() {
            return AbsoluteDate.FUTURE_INFINITY;
        }
        /** {@inheritDoc}
         */
        public SpacecraftState propagate(AbsoluteDate target)
            throws PropagationException
        {
            return AbstractPropagator.this.basicPropagate(target);
        }
    }
    /** 
     * Specialized event handler to stop integration.
     */
    private static class EndDateDetector
        extends AbstractDetector
    {
        /** 
         * Target end date.
         */
        private final AbsoluteDate endDate;


        /** 
         * @param endDate desired end date
         * @param maxCheck maximal check interval
         * @param threshold convergence threshold
         */
        public EndDateDetector(AbsoluteDate endDate, double maxCheck, double threshold) {
            super(maxCheck, threshold);
            this.endDate = endDate;
        }


        /**
         * {@inheritDoc}
         */
        public int eventOccurred(SpacecraftState s, boolean increasing) {
            return STOP;
        }
        /**
         * {@inheritDoc}
         */
        public double g(SpacecraftState s) {
            return s.getDate().durationFrom(endDate);
        }
    }
}
